package org.novelhibernate.support.logging;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationTargetException;
import org.apache.log4j.Logger;
import org.novelhibernate.exception.NovelhibernateException;
import org.novelhibernate.support.logging.annotation.DefaultExceptionHandling;
import org.novelhibernate.support.logging.annotation.ExceptionRegistry;
import org.novelhibernate.support.logging.annotation.ReleaseHandling;
import org.novelhibernate.support.logging.annotation.LogTrigger;

import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;


public class LogHandler implements InvocationHandler {

	
	private static final Logger log = Logger.getLogger(LogHandler.class);
	// the real instance of class
	private Object proxyInstance;

	private GenericLogFactory factory;
	
	// The real instance, in case that the annotation is records on
	// the actual class. Use this when the annotation can not be added
	// on interface but the implementation class
	private Class<?> instance;

	public LogHandler(Object proxy, Class<?> instance, GenericLogFactory factory)
			throws InstantiationException, IllegalAccessException {

		this.factory = factory;
		this.instance = instance;
		// create fundamental instance
		// the proxy here would normally another Proxy instance as well,
		// therefore here
		// is basically creating a invocation chain
		proxyInstance = proxy;

	}

	public LogHandler(Class<?> proxy, Class<?> instance, GenericLogFactory factory)
			throws InstantiationException, IllegalAccessException {

		this.factory = factory;
		this.instance = instance;
		// create fundamental instance
		proxyInstance = proxy.newInstance();

	}

	@Override
	public Object invoke(Object object, Method callMethod, Object[] args)
			throws Throwable {

		Method method = null;
		if (null == args)
			method = (instance == null) ? callMethod : instance
					.getDeclaredMethod(callMethod.getName());
		else {
		    
			/*
			 * do this to avoid this is a generic method, in which case parameters may be T, D etc.
			 */
			Class<?>[] classes = new Class<?>[args.length];
			for (int i = 0; i < args.length; i++){
				
				// if the defined parameter type is not equals to argument
				// then it can be either a primitive type or generic type
				if(args[i].getClass() != callMethod.getParameterTypes()[i]){
					
					// if it is generic type
					if(callMethod.getGenericParameterTypes()[i].getClass() == TypeVariableImpl.class)
						classes[i] = args[i].getClass();
					// otherwise primitive type
					else
						classes[i] = callMethod.getParameterTypes()[i];
				} else				
				  classes[i] = args[i].getClass();
				
				 log.debug("For method " + callMethod.getName() +  ", type is " + classes[i]);
			        
			        
			}

			method = (instance == null) ? callMethod : instance
					.getDeclaredMethod(callMethod.getName(), classes);
		}
      
        log.debug("Log for method: " + method.getName() + " is " + method.isAnnotationPresent(LogTrigger.class));
        
        if (method.isAnnotationPresent(LogTrigger.class)) {

			LogTrigger trigger = method.getAnnotation(LogTrigger.class);
            
			if(!"".equals(trigger.beforeLog()))
			      factory.doGeneralLoggingTasks(method, trigger.beforeLog(), args);

			FinallizedObject finalObject = buildingFinalObjectInstance(method,
					args);

			Object returnObject = null;
			try {

				// only the method of original instance can be called
				returnObject = callMethod.invoke(proxyInstance, args);

			} catch (Throwable t) {
                
				if(!"".equals(trigger.exceptionLog()))
				    factory.doExceptionLoggingTasks(method, trigger.exceptionLog(),
						t, args);

				Throwable cause = (t.getCause()==null)? t : t.getCause();
				// call release function when exception occurs, if there is any
				if (null != finalObject)
					factory.finallizationCallback(finalObject, true);
				// call custom exception handle function, if there is any
				if (method.isAnnotationPresent(ExceptionRegistry.class)) {

					try {

						return factory.exceptionCallback(proxyInstance, method,
								cause, args, instance);

					} catch (NovelhibernateException e) {

						// do nothing, pass carry on the process
				
					}

				}
				// call the default global exception handling function, if there
				// is any
				if (method.isAnnotationPresent(DefaultExceptionHandling.class)) {

					String id = method.getAnnotation(
							 DefaultExceptionHandling.class).identifier();
					
					if(null == factory.getDefaultExceptionHandler(id))
					      throw new NovelhibernateException("No handler registered for id " + id);
					
					try {

						return factory.getDefaultExceptionHandler(id)
								.doExceptionHandling(proxyInstance, method,
										args, cause);
					} catch (NovelhibernateException e) {

						// do nothing, pass carry on the process
					}

				}

				// if there are any custom or default
				if (t instanceof InvocationTargetException)
					throw t.getCause();
				else
				    throw t;

			} finally {

				// call release function when in finally statement, if there is
				// any
				if (null != finalObject)
					factory.finallizationCallback(finalObject, false);

			}
            
			if(!"".equals(trigger.afterLog()))
			       factory.doGeneralLoggingTasks(method, trigger.afterLog(), args);

			return returnObject;
		} else {
			
			try {
			
			  return callMethod.invoke(proxyInstance, args);
			
			} catch (Throwable t){
				
				// if there are any custom or default
				if (t instanceof InvocationTargetException)
					throw t.getCause();
				else
				    throw t;
			}
		}
	}
	
	public Object getProxy() {

		return Proxy.newProxyInstance(
				proxyInstance.getClass().getClassLoader(), proxyInstance
						.getClass().getInterfaces(), this);
	}
	

	private FinallizedObject buildingFinalObjectInstance(Method method,
			Object[] args) throws Throwable {

		ReleaseHandling handler = method.getAnnotation(ReleaseHandling.class);

		if (null == handler)
			return null;

		FinallizedObject finalObject = null;

		finalObject = new FinallizedObject();
		// if the FinallizedObject is in array
		if (-1 != handler.bindedObjectArrayIndex()) {

			Object[] objects = null;
			if (null == args[handler.bindedObjectArrayIndex()]
					|| null != args[handler.bindedObjectArrayIndex()]
					&& ((Object[]) args[handler.bindedObjectArrayIndex()]).length == 0) {

				objects = new Object[1];
				objects[0] = finalObject;
				args[handler.bindedObjectArrayIndex()] = objects;

			} else {

				Object[] existObjects = (Object[]) args[handler
						.bindedObjectArrayIndex()];

				// the last one will be the finallized objects
				objects = new Object[existObjects.length + 1];

				System.arraycopy(existObjects, 0, objects, 0,
						existObjects.length);

				// add to last index of the new array
				objects[existObjects.length] = finalObject;

				args[handler.bindedObjectArrayIndex()] = objects;
			}

			// otherwise it is a parameter in method 
		} else {

			Class<?>[] types = method.getParameterTypes();
			for (int i = 0; i < types.length; i++) {

				if (types[i] == FinallizedObject.class) {
					args[i] = finalObject;
					break;
				}
			}
		}

		return finalObject;
	}
	
}
